package za.org.jcicada.genss.sensor.impl;

import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import za.org.jcicada.genss.sensor.ISamplingStrategy;
import za.org.jcicada.genss.sensor.ISensor;
import za.org.jcicada.genss.sensor.ISensorObserver;

/**
 * This class keeps track of all the sensors and what strategy is currently
 * used to sample each one.
 * @author rcrida
 */
public class SampleReactor {

    /**
     * Logging object used for logging messages
     */
    private final Log logger = LogFactory.getLog(SampleReactor.class);
    
    /**
     * Finest granularity of calls to periodic (10ms)
     */
    private final long PERIOD_DELAY_MS = 10L;
    
    /**
     * Data structure stores relationship between each sensor and the map of observers
     * to sampling strategies that are monitoring it. Although the sampling strategy
     * itself has a handle to a listener that has a handle to the observer, it is not
     * accessible via the standard interfaces so this complicated structure is required.
     * It is also faster than iterating over sampling strategies to find the one for a
     * particular observer.
     */
    Map<ISensor, Map<ISensorObserver, ISamplingStrategy>> sensorObserverSampling = new IdentityHashMap<ISensor, Map<ISensorObserver, ISamplingStrategy>>();
    
    /**
     * Set of periodic sampling strategies called from timer task
     */
    Set<ISamplingStrategy> periodicStrategies = new HashSet<ISamplingStrategy>();
    
    /**
     * Timer object used to schedule periodic sampling.
     */
    Timer timer = new Timer("Timer-" + getClass().getCanonicalName());
    
    /**
     * Constructor creates a timer class for scheduling periodic samples.
     */
    public SampleReactor() {
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                long time = System.currentTimeMillis();
                for (ISamplingStrategy strategy: periodicStrategies) {
                    strategy.periodic(time);
                }
            }
        }, 0, PERIOD_DELAY_MS);
    }

    /**
     * Add a sensor strategy to the reactor for a particular observer. If a strategy already exists
     * for the same sensor then it must be detached and any outstanding
     * scheduled event cancelled.
     *
     * <p>The new strategy is then attached to the sensor for updates and a
     * periodic sample is triggered to schedule the next one.
     */
    public void addSensor(ISensorObserver observer, ISamplingStrategy strategy, ISensor sensor) {
        strategy.attachToSensor(sensor);
        if (!sensorObserverSampling.containsKey(sensor)) {
            sensorObserverSampling.put(sensor, new HashMap<ISensorObserver, ISamplingStrategy>());            
        }
        Map<ISensorObserver, ISamplingStrategy> observerSampling = sensorObserverSampling.get(sensor);
        ISamplingStrategy currentStrategy = observerSampling.get(observer);
        // if there is already a strategy for this sensor then remove it
        if (currentStrategy != null) {
            sensor.deleteObserver(currentStrategy);
            observerSampling.remove(observer);
            periodicStrategies.remove(strategy);
        }
        observerSampling.put(observer, strategy);
        // register periodic strategies for polling
        if (strategy instanceof StrategyPeriodic) {
            periodicStrategies.add(strategy);
        }
        strategy.periodic(System.currentTimeMillis());
    }
    
    /**
     * Remove the specified sensor from the sample reactor. It will detach all observers
     * for the sensor.
     * @param sensor the specified sensor to remove
     * @return the set of observers currently sampling the sensor
     */
    public Set<ISensorObserver> removeSensor(ISensor sensor) {
        Map<ISensorObserver, ISamplingStrategy> observerSampling = sensorObserverSampling.get(sensor);
        if (observerSampling != null) {
            Set<ISensorObserver> observers = new HashSet<ISensorObserver>(observerSampling.keySet());
            for (ISensorObserver observer: observerSampling.keySet()) {
                detachSensor(observer, sensor);
            }
            return observers;
        }
        // return empty set
        return new HashSet<ISensorObserver>();
    }
    
    /**
     * Detach an observer from the specified sensor if it currently has a sampling 
     * strategy attached to it.
     * @param observer the observer that is detaching.
     * @param sensor the sensor to detach from
     * @return true if detach was successful else false if there was no existing attachment
     */
    public void detachSensor(ISensorObserver observer, ISensor sensor) {
        Map<ISensorObserver, ISamplingStrategy> observerSampling = sensorObserverSampling.get(sensor);
        if (observerSampling != null) {
            ISamplingStrategy currentStrategy = observerSampling.remove(observer);
            periodicStrategies.remove(currentStrategy);
            if (currentStrategy != null) {
                currentStrategy.detachFromSensor();
                // check if no strategies left for sensor
                if (observerSampling.size() == 0) {
                    sensorObserverSampling.remove(sensor);
                }
                return;
            }
        }
        logger.warn("Attempting to detach observer that is not currently sampling the sensor.");
    }
    
    /**
     * Get the number of strategies currently registered with the reactor.
     * @return the number of strategies registered
     */
    public int getNumStrategies() {
        int numStrategies = 0;
        for (Map<ISensorObserver, ISamplingStrategy> observerSampling: sensorObserverSampling.values()) {
            numStrategies += observerSampling.size();
        }
        return numStrategies;
    }

    /**
     * Stop the timer object.
     */
    public void stop() {
        timer.cancel();
    }
    
}
